import java.util.Arrays;
import java.util.HashMap;
import java.util.Random;
import java.util.Vector;

/*
 * 1. This class will have just one object among all the bit torrent threads
 * 2. PeerID read in the contructor
 */

public class PeerData {

	/*
	 * - globalBit Vectors : [peer id][piece count] - Hash Maps is needed to
	 * convert PeerId to peerNumber - PeerNum varies from 0 to n-1
	 */
	private int peerID;
	private int numOfPeers;
	private int totalNumOfPieces;
	private boolean[][] globalFilePieceVector;
	private HashMap<Integer, Integer> peerIDHash = new HashMap<Integer, Integer>();
	private HashMap<Integer, Integer> peerNumHash = new HashMap<Integer, Integer>();

	private int numberOfPreferredNeighbors;
	private int unchokingInterval;
	private int optimisticUnchokingInterval;
	private long fileSize;
	private boolean interestedReceived[];
	private boolean interestedSend[];
	private int downloadSpeed[];
	private boolean hasCompleteFile;
	private Random randomGenerator;	

	private boolean shutdown = false;

	/*
	 * To update the choking or enter data into queues interested peers que and
	 * peer whom this peer has sent the ineterested Message
	 */

	/*
	 * The signature has been changed
	 */
	PeerData(FileProperties prop, int peerID) {
		initPeerHash(prop.peerIDArray);
		globalFilePieceVector = new boolean[prop.getNumPeers()][prop
				.getTotalNumPieces()];
		this.peerID = peerID;
		this.numOfPeers = prop.peerIDArray.length;
		this.totalNumOfPieces = prop.getTotalNumPieces();
		interestedReceived = new boolean[prop.getNumPeers()];
		interestedSend = new boolean[prop.getNumPeers()];
		downloadSpeed = new int[prop.getNumPeers()];
		if (prop.hasCompleteFile()) {
			initBitFields();
			hasCompleteFile = true;
		}
		randomGenerator = new Random();
		numberOfPreferredNeighbors = prop.getNumberOfPreferredNeighbors();
		unchokingInterval = prop.getUnchokingInterval();
		optimisticUnchokingInterval = prop.getOptimisticUnchokingInterval();
		fileSize = prop.getFileSize();
		totalNumOfPieces = prop.getTotalNumPieces();
	}
	
	public static void main(String args[] ){
		PeerData pr = new PeerData(new FileProperties(true, 1002), 1002);
		System.out.println(pr.peerIDHash.get(1002));
	}
	
	public int getTotalNumOfPieces() {
		return totalNumOfPieces;
	}



	public boolean hasCompleteFile() {
		return hasCompleteFile;
	}

	/*
	 * PlaceHolder Function
	 */
	public void initPeerHash(int[] peerIDArray) {
		for (int i = 0; i < peerIDArray.length; i++) {
			peerIDHash.put(peerIDArray[i], i);
			peerNumHash.put(i, peerIDArray[i]);
			// System.out.println(peerIDArray[0]);
		}
	}

	/*
	 * Says that you have all the complete file
	 */
	private void initBitFields() {
		for (int i = 0; i < totalNumOfPieces; i++) {
			globalFilePieceVector[peerIDHash.get(peerID)][i] = true;
		}
	}

	/*
	 * This method updates all the bitfields about a certain peer into pur bit
	 * vector
	 */
	public synchronized void updateBitfield(int otherPeerID, byte[] bitField) {
		int count = 0;
		for (int i = 0; i < bitField.length; i++) {
			if ((bitField[i] & 0x80) == 0x80) {
				globalFilePieceVector[peerIDHash.get(otherPeerID)][count] = true;
			}
			count++;
			if ((bitField[i] & 0x40) == 0x40) {
				globalFilePieceVector[peerIDHash.get(otherPeerID)][count] = true;
			}
			count++;
			if ((bitField[i] & 0x20) == 0x20) {
				globalFilePieceVector[peerIDHash.get(otherPeerID)][count] = true;
			}
			count++;
			if ((bitField[i] & 0x10) == 0x10) {
				globalFilePieceVector[peerIDHash.get(otherPeerID)][count] = true;
			}
			count++;
			if ((bitField[i] & 0x08) == 0x08) {
				globalFilePieceVector[peerIDHash.get(otherPeerID)][count] = true;
			}
			count++;
			if ((bitField[i] & 0x04) == 0x04) {
				globalFilePieceVector[peerIDHash.get(otherPeerID)][count] = true;
			}
			count++;
			if ((bitField[i] & 0x02) == 0x02) {
				globalFilePieceVector[peerIDHash.get(otherPeerID)][count] = true;
			}
			count++;
			if ((bitField[i] & 0x01) == 0x01) {
				globalFilePieceVector[peerIDHash.get(otherPeerID)][count] = true;
			}
			count++;
		}
	}

	/*
	 * private void updateBit(int bit){ if (bit >=
	 * globalFilePieceVector[0].length){ // insert an assert System.out.println(
	 * "Update Bit field trying to access more pieces than what exists"); } if
	 * ((bitField[i] & 0x80) == 0x80){
	 * globalFilePieceVector[peerIDHash.get(otherPeerID)][count] = true; }
	 * count++; }
	 */

	/*
	 * This method return true if the other peer has any interesting data with
	 * him
	 */
	public synchronized boolean checkInterested(int otherPeerID) {
		for (int i = 0; i < totalNumOfPieces; i++) {
			if (globalFilePieceVector[peerIDHash.get(peerID)][i] == false
					&& globalFilePieceVector[peerIDHash.get(otherPeerID)][i] == true) {
				return true;
			}
		}
		return false;
	}

	/*
	 * This method returns a piece number which you might be interested from the
	 * other peer
	 */
	public int requestPeice(int otherPeerID) {
		for (int i = 0; i < totalNumOfPieces; i++) {
			if (globalFilePieceVector[peerIDHash.get(peerID)][i] == false
					&& globalFilePieceVector[peerIDHash.get(otherPeerID)][i] == true) {
				return i;
			}
		}
		return -1;
	}

	public int getPeerID() {
		return peerID;
	}

	public void printBitVectors() {
		for (int i = 0; i < numOfPeers; i++) {
			System.out.print(i + ": ");
			for (int j = 0; j < totalNumOfPieces; j++) {
				if (globalFilePieceVector[i][j])
					System.out.print(1 + " ");
				else
					System.out.print(0 + " ");
			}
			System.out.println();
		}
	}

	public synchronized void incrementDownloadSpeed(int peerID) {
		downloadSpeed[peerIDHash.get(peerID)] += 1;
	}

	/*
	 * get top n elements ... do you have peerID, how do you want it
	 */
	public synchronized int getDownloadSpeed(int peerID) {
		return downloadSpeed[peerIDHash.get(peerID)];
	}

	/*
	 * The below 4 functions correspond to interestedSend and interestedReceive!
	 */
	public synchronized void addToInterestSend(int peerID) {
		Integer peerNum = peerIDHash.get(peerID);
		if (peerNum != null) {
			if (!interestedSend[peerNum])
				interestedSend[peerNum] = true;
		}
	}

	public synchronized void addToInterestReceived(int peerID) {
		Integer peerNum = peerIDHash.get(peerID);
		if (peerNum != null) {
			if (!interestedReceived[peerNum])
				interestedReceived[peerNum] = true;
		}
	}

	public synchronized void removeFromInterestReceived(int peerID) {
		Integer peerNum = peerIDHash.get(peerID);
		if (peerNum != null) {
			if (interestedReceived[peerNum])
				interestedReceived[peerNum] = false;
		}
	}

	public synchronized void removeFromInterestSend(int peerID) {
		Integer peerNum = peerIDHash.get(peerID);
		if (peerNum != null) {
			if (interestedSend[peerNum])
				interestedSend[peerNum] = false;
		}
	}

	public synchronized int updateBitfield(int otherPeerID, int pieceNum) {
		int count = 0;
		System.out.println("Update bit field PeerNum:"+peerIDHash.get(otherPeerID)+" pieceNum: "+pieceNum);
		if (!globalFilePieceVector[peerIDHash.get(otherPeerID)][pieceNum]) {
			globalFilePieceVector[peerIDHash.get(otherPeerID)][pieceNum] = true;

			for (int i = 0; i < totalNumOfPieces; i++) {
				if (globalFilePieceVector[peerIDHash.get(otherPeerID)][i]) {
					count++;
				}
			}
		} else {
			return -1;
		}

		return count;
	}

	public synchronized int getInterestedPieceNumber(int otherPeerID) {
		// Compare peerID bitfield and otherPeerID bitfield to get required
		// piece numbers
		// Pick a random piece number from the above set and send
		int tempPieceNumArray[] = new int[totalNumOfPieces];
		int count = 0;
		for (int i = 0; i < totalNumOfPieces; i++) {
			if (globalFilePieceVector[peerIDHash.get(peerID)][i] == false
					&& globalFilePieceVector[peerIDHash.get(otherPeerID)][i] == true) {
				tempPieceNumArray[count++] = i;
			}
		}
		if(count>0){
		Random r = new Random();
		return tempPieceNumArray[r.nextInt(count)];
		}
		return -1;

	}

	public synchronized int getPeerNum(int otherPeerID) {
		return peerIDHash.get(otherPeerID);
	}

	public synchronized int getPeerID(int peerNum) {
		return peerNumHash.get(peerNum);
	}

	public synchronized boolean[] getInterestedSend() {
		return interestedSend;
	}

	public synchronized void setInterestedSend(boolean[] interestedSend) {
		this.interestedSend = interestedSend;
	}

	public synchronized boolean checkForTermination() {

		if (!shutdown) {
			for (int i = 0; i < globalFilePieceVector.length; i++) {
				for (int j = 0; j < globalFilePieceVector[i].length; j++) {
					if (globalFilePieceVector[i][j] == false)
						return false;
				}
			}
			shutdown = true;
		}
		return shutdown;
	}
	
	public boolean checkCloseConnection(int otherPeerID){
		
		for (int j = 0; j < globalFilePieceVector[getPeerNum(peerID)].length; j++) {
			if (globalFilePieceVector[getPeerNum(peerID)][j] == false || globalFilePieceVector[getPeerNum(otherPeerID)][j] == false)
				return false;
		}
		return true;
	}

	static String byteToString(byte b){
		// there is also a better way to do this
		//System.out.println(Integer.toBinaryString(getPayload()[1]));
		String str = "";
		for(int i = 0; i < 8 ; i++){
			//System.out.println(0x1<<i);
			if ((0x1<<i & b) == 0x1<<i){
				
				str = '1' + str;//getPayload()[0];
			}else{
				str = '0' + str;
			}
			
		}
		return str;
	}

	 
		public synchronized int getNumberOfPreferredNeighbors() {
			return numberOfPreferredNeighbors;
		}

		public synchronized void setNumberOfPreferredNeighbors(
				int numberOfPreferredNeighbors) {
			this.numberOfPreferredNeighbors = numberOfPreferredNeighbors;
		}

		public synchronized int getUnchokingInterval() {
			return unchokingInterval;
		}

		public synchronized void setUnchokingInterval(int unchokingInterval) {
			this.unchokingInterval = unchokingInterval;
		}

		public synchronized int getOptimisticUnchokingInterval() {
			return optimisticUnchokingInterval;
		}

		public synchronized void setOptimisticUnchokingInterval(
				int optimisticUnchokingInterval) {
			this.optimisticUnchokingInterval = optimisticUnchokingInterval;
		}

		public synchronized int getNumOfPeers() {
			return numOfPeers;
		}
		
 // Functions for Preferred Neighbor selection
        
        // returns an array with peerNumber of k neighbors who provided highest download speed in last unchoking interval and are interested
        // return array value: -1 for no neighbor (<k interested)
	public synchronized int[] topKNeighbors(int k){
        int[] topKNeighbors = new int[k];
        int index = 0;
        int[] sortedDownloadSpeed = createAndSort(downloadSpeed);
        
        // Get the top K neighbors interested
        for(int peerNum=0; peerNum<numOfPeers && index<k; peerNum++){
                if(interestedReceived[peerNum] && containsInFirstK(sortedDownloadSpeed, downloadSpeed[peerNum], k)){
                        topKNeighbors[index++] = peerNum;
                }
        }
        // Set the remaining places to -1: indicates <k neighbors interested
        if(index!=k){
                while(index<k)
                        topKNeighbors[index++] = -1;
        }
        
        return topKNeighbors;
}
 
     // helper function
     public boolean containsInFirstK(int array[], int checkMe, int k){
             for(int i=0; i<k; i++){
                     if(array[i] == checkMe)
                             return true;
             }
             
             return false;
     }
     
     // helper function - creates new array from input array, sorts in descending order and returns new array
     // TODO: check for concurrency on downloadSpeed[]
     private synchronized int[] createAndSort(int[] sortMe){
             int[] newArray = new int[sortMe.length];
             int index = 0;
             for(int i=0; i<sortMe.length; i++){
                 if(interestedReceived[i])
                 	newArray[index++] = sortMe[i];
             }
             Arrays.sort(newArray);
             // reverse array to bring in descending order
             for (int i=0; i<newArray.length/2; i++){
                     int temp = newArray[newArray.length - 1 - i];
                     newArray[newArray.length - 1 - i] = newArray[i];
                     newArray[i] = temp;
             }
             
             return newArray;
     }
     
     // Reset download of all neighbors but input param to zero
     public synchronized void resetDownloadExcept(int exceptPeerNum){
             for(int i=0; i<downloadSpeed.length; i++){
                     if(i != exceptPeerNum)
                             downloadSpeed[i] = 0;
             }
     }
     
     // returns peerNum for next guy to be opt. unchoked: who is interested but not in currently unchokedNeighbors
     // returns -1 if no neighbor to unchoke
     public synchronized int nextOptUnchokedNeighbor(boolean[] unchokedNeighbors){
    	 int numOfNotUnchoked = 0;
		 Vector<Integer> notUnchoked = new Vector<Integer>(numOfPeers);
	     for(int peerNum=0; peerNum<numOfPeers; peerNum++){
	             if(interestedReceived[peerNum] && !unchokedNeighbors[peerNum]){
	                     notUnchoked.add(peerNum);
	             }                               
	     }
	     numOfNotUnchoked = notUnchoked.size();
	     if(numOfNotUnchoked == 0){
	     	return -1;
	     } else {
	            int random = randomGenerator.nextInt(notUnchoked.size());
	            return (Integer) notUnchoked.get(random);
	     }
     }
}
